iT邦幫忙

2024 iThome 鐵人賽

DAY 3
1
Python

為你自己讀 CPython 原始碼系列 第 3

Day 3 - 全部都是物件!(上)

  • 分享至 

  • xImage
  •  

本文同步刊載於 「為你自己學 Python - 全部都是物件!(上)

全部都是物件!(上)

為你自己學 Python

在大部份的 Python 程式教學裡,常會聽到在 Python 裡什麼東西都是物件這樣的說法。整數、浮點數是物件、字串是物件,串列、字典不用講它看起來就像個物件,甚至連函數跟類別也都是物件。既然都是物件,這個章節就先來看看在 CPython 裡的物件長什麼樣子。

什麼是「物件(Object)」?

在 CPython 裡的物件是使用 PyObject 這個「結構(Struct)」來表示的,我們先來看看 PyObject 是怎麼定義的。在 Include/pytypedefs.h 檔案裡有寫到:

// 檔案:Include/pytypedefs.h

// ... 略 ...
typedef struct PyGetSetDef PyGetSetDef;
typedef struct PyMemberDef PyMemberDef;

typedef struct _object PyObject;
typedef struct _longobject PyLongObject;
typedef struct _typeobject PyTypeObject;
typedef struct PyCodeObject PyCodeObject;
// ... 略 ...

PyObject 果然是個 struct 沒錯,再順著這個 _object 繼續找下去,會發現在 Include/object.h 的第 166 行左右可以看到 _object 這個結構的定義:

// 檔案:Include/object.h

struct _object {
    _PyObject_HEAD_EXTRA

#if (defined(__GNUC__) || defined(__clang__)) \
        && !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L)
    // On C99 and older, anonymous union is a GCC and clang extension
    __extension__
#endif
#ifdef _MSC_VER
    // Ignore MSC warning C4201: "nonstandard extension used:
    // nameless struct/union"
    __pragma(warning(push))
    __pragma(warning(disable: 4201))
#endif
    union {
       Py_ssize_t ob_refcnt;
#if SIZEOF_VOID_P > 4
       PY_UINT32_T ob_refcnt_split[2];
#endif
    };
#ifdef _MSC_VER
    __pragma(warning(pop))
#endif

    PyTypeObject *ob_type;
};

看起來有點複雜,我把這裡我把像是 #ifdef 條件編譯指令先拿掉,先看重點就好,現在看起來就會只剩下這樣:

// 檔案:Include/object.h

struct _object {
    _PyObject_HEAD_EXTRA

    union {
       Py_ssize_t ob_refcnt;
    };

    PyTypeObject *ob_type;
};

_object 結構看起來還算簡單,在這個結構裡有一個 _PyObject_HEAD_EXTRA 巨集以及兩個成員變數,分別是 ob_refcntob_type,我們就一個一個來看。

上一個與下一個

先來看看最開頭的 _PyObject_HEAD_EXTRA。這是什麼?追一下應該會在同一個檔案裡看到它的定義:

// 檔案:Include/object.h

#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
    PyObject *_ob_next;           \
    PyObject *_ob_prev;

#define _PyObject_EXTRA_INIT _Py_NULL, _Py_NULL,

#else
#  define _PyObject_HEAD_EXTRA
#  define _PyObject_EXTRA_INIT
#endif

在 C 語言裡 #define 是用來定義「巨集(Macro)」用的,巨集不是變數也不是函數,而是在進行編譯的過程中會被替換成指定的程式碼片段,主要的目的是讓程式碼更容易閱讀,也能被重複使用。

不知道你有沒有注意到在巨集上面那行註解:

Define pointers to support a doubly-linked list of all live heap objects.

也就是說,其實 _PyObject_HEAD_EXTRA 這個巨集就是 _ob_next_ob_prev 這兩個指標,是用來串接所有的 Python 物件,形成一個雙向鏈結串列(Doubly-Linked List)的結構。Python 的物件是活在 Heap 上的,Heap 不像 Stack 是連續的記憶體位置,這些物件宣告之後可能會散落在 Heap 上,Python 要怎麼知道目前這個物件的下一個或上一個物件是什麼?Python 使用「雙向鏈結串列(Doubly-Linked List)」的資料結構來串接所有的物件。雙向鏈結串列的好處是可以容易的找到下一個或上一個物件,而且在新增或刪除物件的時候也很有效率(O(1)),但缺點就是在搜尋的時候會比較慢(O(n))。

從原始碼可以看的出來,_PyObject_HEAD_EXTRA 巨集在一般的編譯過程通常是空值,只有在定義了 Py_TRACE_REFS 時,才會包含 _ob_next_ob_prev 這兩個指標,通常只有在想要追蹤 Python 物件的引用計數時才會開啟這個功能,也就是說我們一般常用的 Python 版本,物件之間並不會形成一個雙向鏈結串列。

資源回收機制

ob_refcnt 是一個 Py_ssize_t 型態的變數,一路追下去就會看到它其實是個長整數(long),ob_refcntrefcnt 是 Reference Count(RC)的意思,這是 Python 處理資源回收的做法。當物件被引用的時候,該物件身上的 RC 會增加,反之如果引用不存在了,RC 就會減少,當 RC 降到變成 0 的時候,表示這顆物件就沒人要了,可以準備被 Garbage Collection(GC)給回收掉了,然後把原本物件佔用的資源還給系統。

在這裡 ob_refcnt 被包在一個 union 裡,通常這是為了在不同的編譯條件下用來擴充或變更結構體欄位的做法。繼續來追一下 ob_refcnt 的初始值是怎麼設定的:

// 檔案:Objects/object.c

PyObject *
_PyObject_New(PyTypeObject *tp)
{
    PyObject *op = (PyObject *) PyObject_Malloc(_PyObject_SIZE(tp));
    if (op == NULL) {
        return PyErr_NoMemory();
    }
    _PyObject_Init(op, tp);
    return op;
}

這個 _PyObject_New() 函數的用途滿好猜的,會根據傳入的參數型別去要一塊記憶體位置,然後對物件進行初始化,簡單的說,就是一個用來建立 PyObject 物件的函數。順著 _PyObject_Init() 函數往下追:

// 檔案:Include/internal/pycore_object.h

static inline void
_PyObject_Init(PyObject *op, PyTypeObject *typeobj)
{
    assert(op != NULL);
    Py_SET_TYPE(op, typeobj);
    if (_PyType_HasFeature(typeobj, Py_TPFLAGS_HEAPTYPE)) {
        Py_INCREF(typeobj);
    }
    _Py_NewReference(op);
}

可以看到最後面 _Py_NewReference() 函數,這個函數應該就是用來設定 RC 的,繼續往下追:

// 檔案:Include/internal/pycore_object.h

void
_Py_NewReference(PyObject *op)
{
    new_reference(op);
}

快抓到你了,最後再追一下 new_reference() 函數:

// 檔案:Objects/object.c

static inline void
new_reference(PyObject *op)
{
    if (_PyRuntime.tracemalloc.config.tracing) {
        _PyTraceMalloc_NewReference(op);
    }
    // Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
    op->ob_refcnt = 1;
}

啊哈!就是這裡啦,可以看到這個函數會把傳進來的 PyObject 的 ob_refcnt 設定成 1。也就是說,所有剛剛出生的物件的 ob_refcnt 的預設值都是 1。為什麼要這樣做?想想看,如果沒有先把新物件的 RC 初始化成 1,那不就表示這顆物件一生出來立刻就準備要被收掉了嗎?

既然都看這裡了,順便再看 RC 是怎麼增加以及減少的:

// 檔案:Include/object.h

static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op)
{
    // Explicitly check immortality against the immortal value
    if (_Py_IsImmortal(op)) {
        return;
    }
    op->ob_refcnt++;
}

就是個簡單的 ++ 做遞增而已,那減少呢?

// 檔案:Include/object.h

static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
    // Non-limited C API and limited C API for Python 3.9 and older access
    // directly PyObject.ob_refcnt.
    if (_Py_IsImmortal(op)) {
        return;
    }
    _Py_DECREF_STAT_INC();
    if (--op->ob_refcnt == 0) {
        _Py_Dealloc(op);
    }
}

減少就是使用 -- 進行遞減而已,不過這裡可以看到當 RC 降為 0 的時候,就會呼叫 _Py_Dealloc() 函數,再往裡追就看到這個物件的生命週期,例如會呼叫那顆物件身上的 tp_dealloc 方法,不過這個細節等介紹到 Type 的時候再來介紹。

不死身物件

不知道大家在這裡有沒有在原始碼裡發現「不死身(Immortal)」的設計?仔細看的話,會發現不管 RC 在增加或減少的時候,遇到不死身物件就會當做沒這回事,不會去改變 RC 的值。來看一下用來判斷是否為不死身物件的 _Py_IsImmortal() 函數是怎麼實作的:

// 檔案:Include/object.h

static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op)
{
#if SIZEOF_VOID_P > 4
    return _Py_CAST(PY_INT32_T, op->ob_refcnt) < 0;
#else
    return op->ob_refcnt == _Py_IMMORTAL_REFCNT;
#endif
}

大概可以猜的出來,只要把 ob_refcnt 設定成一個特殊的值,就會被當做不死身,這個特殊的值是什麼呢?這個值在 64 位元以上的作業系統上是小於 0 的數字,而在 32 位系統上,引用計數的範圍較小,Python 選擇了一個特定的固定值來標識不死身物件,而不是使用負值。

為什麼要做這樣的設計?這是因為在 Python 裡有些物件是不會也不需要被回收的,為了讓引用計數的操作更有效率,CPython 將某些全局唯一的物件例如 NoneTrue 以及 False 這些值。這樣可以避免不必要的 RC 計算,讓效能好一些。

PyTypeObject

PyObject 的最後一個 ob_type 是一個指向 PyTypeObject 的指標,這個 PyTypeObject 是什麼呢?

// 檔案:Include/pytypedefs.h

typedef struct _object PyObject;
typedef struct _longobject PyLongObject;
typedef struct _typeobject PyTypeObject;
typedef struct PyCodeObject PyCodeObject;
typedef struct _frame PyFrameObject;

看來 PyTypeObject 就是 _typeobject 這個結構的別名,繼續追一下 _typeobject 的定義,結果查到一個有點大的結構:

// 檔案:Include/object.h

struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;

    // ... 略 ...

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;
};

這個結構裡面有很多成員變數,像是 tp_name 就是這個類別的名字,tp_dealloc 是這個類別的解構函數,不同的類別在做解構的時候可能都會有自己要做的事情,另外 tp_doc 就是我們在印出物件的 .__doc__ 屬性的時候會被印出來的傢伙。我們可以動手改E一下 tp_name 的值,然後重新編譯 CPython,看看會發生什麼事情。這裡我拿負責串列(List)的 PyList_Type 來改一下:

// 檔案:Objects/listobject.c

PyTypeObject PyList_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "list-哈囉你好",  // <-- 改這裡
    sizeof(PyListObject),
    0,
    (destructor)list_dealloc,                   /* tp_dealloc */
    // ... 略 ...
    PyObject_GC_Del,                            /* tp_free */
    .tp_vectorcall = list_vectorcall,
};

把原本的 "list" 改成 "list-哈囉你好",重新 make 編譯之後,在 Python 的 REPL 裡透過內建函數 type() 來印出它的型別的時候,型別名字就會變成 list-哈囉你好 了:

$ ./python.exe
Python 3.12.6+ (heads/code-review-dirty:914b9826fe6, Sep 17 2024, 17:11:15) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
=============
Hello CPython
=============
>>> a = [1, 2, 3]
>>> type(a)
<class 'list-哈囉你好'>
>>>

其實這沒什麼用,別這麼做,這只是為了好玩示範一下而已。

更多 _typeobject 的成員變數待後續章節有遇到的時候再來詳細說明,這裡我們就只先看看最開頭的 PyObject_VAR_HEAD

// 檔案:Include/object.h

#define PyObject_VAR_HEAD      PyVarObject ob_base;

再繼續往下追 PyVarObject 的定義,就會發現這段定義就剛好放在最一開始提到的 _object 旁邊而已:

// 檔案:Include/object.h

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

這個 PyVarObject 裡面有兩個成員變數,其中 ob_base 就是一個 PyObject 而已,另一個是 ob_size 是用來記錄或表示這個物件的「數量」。什麼意思?例如以串列(List)來說,ob_size 就是串列裡面的元素個數,對字串來說就是 ob_size 就是字串的長度。對於可變動的物件,像是 List,當增加或刪除元素的時候,ob_size 也會跟著變動,相對的,對於不可變動的物件,像是字串或是 Tuple,ob_size 就是固定的,不會變動,更精準的說,是不可變物件沒有提供可以修改 ob_size 的方法。

小結

PyObject 是 CPython 裡的物件結構,這個結構體在 Python 的內部運作中扮演著核心角色,其中 ob_refcnt 是用來計算物件的引用次數,當引用次數為 0 時,這個物件就會被回收。而 ob_type 則是指向 PyTypeObject 的指標,這個 PyTypeObject 是用來描述物件的型別,包含了物件的行為和特性,整個 Python 的物件導向幾乎就是繞著它轉。正是有 ob_type 的存在,每個 PyObject 才能有不同的型別,能夠實現物件導向的特性,讓物件能夠進行多型(Polymorphism)和繼承(Inheritance)等操作。

PyTypeObject 這個結構暫時先看到這裡,下個章節,我們先來看看在 Python 的世界建立一個物件的過程。

本文同步刊載於 「為你自己學 Python - 全部都是物件!(上)


上一篇
Day 2 - CPython 專案簡介
下一篇
Day 4 - 物件生成全紀錄
系列文
為你自己讀 CPython 原始碼4
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言